<template>
  <div class="vlist-dynamic">
    <div ref="containRef" class="container" @scroll="scrollEvent">
      <div class="phantom" :style="{ height: phantomHeight + 'px' }"></div>
      <div class="content" :style="{ transform: getTransform }">
        <div v-for="item in visibleData" :key="item.index" class="content-item" ref="itemRefs" :id="item.index">
          {{ item.text }}
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { faker } from '@faker-js/faker';
import { computed, onMounted, onUpdated, reactive, ref } from 'vue';
const listData = ref(new Array(1000).fill({}).map((item, index) => ({ index, text: faker.lorem.sentences() })))
const estimateHeight = ref(100)
const itemPostions = ref({})
const itemRefs = ref(null)
const visibleInfo = reactive({
  startInx: 0,
  endInx: 0,
  height: 0,
  count: 0
})
const visibleData = computed(() => {
  return listData.value.slice(visibleInfo.startInx, Math.min(visibleInfo.endInx, listData.value.length))
})
const phantomHeight = ref(0)
const containRef = ref(null)
const startOffset = ref(0)
const getTransform = computed(() => {
  return `translate3d(0, ${startOffset.value}px, 0)`
})
onMounted(() => {
  visibleInfo.height = containRef.value.clientHeight
  phantomHeight.value = estimateHeight.value * listData.value.length
  visibleInfo.startInx = 0
  visibleInfo.count = Math.ceil(visibleInfo.height / estimateHeight.value)
  visibleInfo.endInx = visibleInfo.startInx + visibleInfo.count
  itemPostions.value = listData.value.map((item, index) => {
    return {
      index,
      top: index*estimateHeight.value,
      bottom: (index + 1) * estimateHeight.value,
      height: estimateHeight.value
    }
  })
})
onUpdated(() => {
  if(!itemRefs.value || !itemRefs.value.length) return
  computeVisualSize()
  phantomHeight.value = itemPostions.value[itemPostions.value.length - 1].bottom
  getOffsetY()
})
const computeVisualSize = () => {
  itemRefs.value.map(item => {
    const id = +item.id
    const curHeight = item.clientHeight
    const oldHeight = itemPostions.value[id].height
    const dValue = curHeight - oldHeight
    if(dValue) {
      itemPostions.value[id].height = curHeight
      itemPostions.value[id].bottom = itemPostions.value[id].bottom + dValue
      for(let i = id + 1; i < itemPostions.value.length; i++) {
        itemPostions.value[i].top = itemPostions.value[i].top + dValue
        itemPostions.value[i].bottom = itemPostions.value[i].bottom + dValue
      }
    }
  })
}
const getOffsetY = () => {
  startOffset.value = visibleInfo.startInx >= 1 ? itemPostions.value[visibleInfo.startInx].top : 0
}
const scrollEvent = (e) => {
  const scrollTop = e.target.scrollTop
  visibleInfo.startInx = getStartInx(scrollTop)
  visibleInfo.endInx = visibleInfo.startInx + visibleInfo.count
  startOffset.value = getOffsetY()
}
const getStartInx = (scrollTop) => {
  let start = 0
  let end = listData.value.length - 1
  let tempIndex = null
  while(start <= end) {
    const mid = (start + end) >> 1
    const midBottom = itemPostions.value[mid].bottom
    if(midBottom === scrollTop) {
      tempIndex = mid + 1
      return tempIndex
    } else if(midBottom < scrollTop) {
      start = mid + 1
    } else {
      if(tempIndex === null || tempIndex > mid) {
        tempIndex = mid
      }
      end = end - 1
    }
  }
  return tempIndex
}
</script>